package tools.jackson.databind.ext.javatime.deser;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

import tools.jackson.core.*;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.cfg.DateTimeFeature;
import tools.jackson.databind.exc.InvalidFormatException;

import com.fasterxml.jackson.annotation.JsonFormat;

/**
 * Deserializer for Java 8 temporal {@link Month}s.
 */
public class MonthDeserializer extends JSR310DateTimeDeserializerBase<Month>
{
    public static final MonthDeserializer INSTANCE = new MonthDeserializer();

    private final Set<String> possibleMonthStringValues = Arrays.stream(Month.values()).map(Month::name).collect(Collectors.toSet());

    /**
     * NOTE: only {@code public} so that use via annotations (see [modules-java8#202])
     * is possible
     */
    public MonthDeserializer() {
        this(null);
    }

    public MonthDeserializer(DateTimeFormatter formatter) {
        super(Month.class, formatter);
    }

    protected MonthDeserializer(MonthDeserializer base, Boolean leniency) {
        super(base, leniency);
    }

    protected MonthDeserializer(MonthDeserializer base,
                                   Boolean leniency,
                                   DateTimeFormatter formatter,
                                   JsonFormat.Shape shape) {
        super(base, leniency, formatter, shape);
    }

    @Override
    protected MonthDeserializer withLeniency(Boolean leniency) {
        return new MonthDeserializer(this, leniency);
    }

    @Override
    protected MonthDeserializer withDateFormat(DateTimeFormatter dtf) {
        return new MonthDeserializer(this, _isLenient, dtf, _shape);
    }

    @Override
    public Month deserialize(JsonParser p, DeserializationContext ctxt)
            throws JacksonException
    {
        if (p.hasToken(JsonToken.VALUE_STRING)) {
            return _fromString(p, ctxt, p.getString());
        }
        // Support numeric scalar input
        if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
            final int raw = p.getIntValue();
            if (ctxt.isEnabled(DateTimeFeature.ONE_BASED_MONTHS)) {
                return _decodeMonth(raw, ctxt);
            }
            // default: 0‑based index (0 == JANUARY)
            if (raw < 0 || raw >= 12) {
                ctxt.handleWeirdNumberValue(handledType(),
                        raw, "Month index (%s) outside 0-11 range", raw);
                return null; // never gets here, but compiler doesn't know
            }
            return Month.values()[raw];
        }
        // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
        if (p.isExpectedStartObjectToken()) {
            final String str = ctxt.extractScalarFromObject(p, this, handledType());
            // 17-May-2025, tatu: [databind#4656] need to check for `null`
            if (str != null) {
                return _fromString(p, ctxt, str);
            }
            // fall through
        } else if (p.isExpectedStartArrayToken()) {
            JsonToken t = p.nextToken();
            if (t == JsonToken.END_ARRAY) {
                return null;
            }
            if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT)
                    && ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
                final Month parsed = deserialize(p, ctxt);
                if (p.nextToken() != JsonToken.END_ARRAY) {
                    handleMissingEndArrayForSingle(p, ctxt);
                }
                return parsed;
            }
            if (t != JsonToken.VALUE_NUMBER_INT) {
                _reportWrongToken(ctxt, JsonToken.VALUE_NUMBER_INT, Integer.class.getName());
            }
            int month = p.getIntValue();
            if (p.nextToken() != JsonToken.END_ARRAY) {
                throw ctxt.wrongTokenException(p, handledType(), JsonToken.END_ARRAY,
                        "Expected array to end");
            }
            return Month.of(month);
        } else if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
            return (Month) p.getEmbeddedObject();
        }
        return _handleUnexpectedToken(ctxt, p,
                JsonToken.VALUE_STRING, JsonToken.START_ARRAY);
    }

    protected Month _fromString(JsonParser p, DeserializationContext ctxt,
                                   String string0)
            throws JacksonException
    {
        String string = string0.trim();
        if (string.length() == 0) {
            // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish
            //   b/w empty and blank); for now don't which will allow blanks to be
            //   handled like "regular" empty (same as pre-2.12)
            return _fromEmptyString(p, ctxt, string);
        }
        try {
            if (_formatter == null) {
                // First: try purely numeric input
                try {
                    int oneBasedMonthNumber = Integer.parseInt(string);
                    if (ctxt.isEnabled(DateTimeFeature.ONE_BASED_MONTHS)) {
                        return _decodeMonth(oneBasedMonthNumber, ctxt);
                    }
                    if (oneBasedMonthNumber < 0 || oneBasedMonthNumber >= 12) { // invalid for 0‑based
                        throw new InvalidFormatException(p, "Month number " + oneBasedMonthNumber + " not allowed for 1-based Month.", oneBasedMonthNumber, Integer.class);
                    }
                    return Month.values()[oneBasedMonthNumber]; // 0‑based mapping
                } catch (NumberFormatException nfe) {
                    // fall through – treat as textual month name
                }
                // Second: try textual input
                // Handle English month names such as "JANUARY" from the actual Month Enum names
                if (possibleMonthStringValues.contains(string)) {
                    return Month.valueOf(string);
                } else {
                    throw new InvalidFormatException(p, String.format("Cannot deserialize value of type `java.time.Month` from String \"%s\": not one of the values accepted for Enum class: %s", string, Arrays.toString(Month.values())), string, Month.class);
                }
            }
            return Month.from(_formatter.parse(string));
        } catch (DateTimeException e) {
            return _handleDateTimeFormatException(ctxt, e, _formatter, string);
        } catch (NumberFormatException e) {
            throw ctxt.weirdStringException(string, handledType(),
                    "not a valid month value");
        }
    }

    /**
     * Validate and convert a 1‑based month number to {@link Month}.
     */
    private Month _decodeMonth(int oneBasedMonthNumber, DeserializationContext ctxt)
            throws JacksonException
    {
        if (Month.JANUARY.getValue() <= oneBasedMonthNumber && oneBasedMonthNumber <= Month.DECEMBER.getValue()) {
            return Month.of(oneBasedMonthNumber);
        }
        // If out of range, throw an exception
        ctxt.handleWeirdNumberValue(handledType(),
                oneBasedMonthNumber, "Month number %s not allowed for 1-based Month.", oneBasedMonthNumber);
        return null; // never gets here, but compiler doesn't know
    }
}
